package com.tuniu.agents.util;

import java.io.IOException;
import java.time.Duration;
import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

import org.springframework.http.HttpHeaders;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.multipart.MultipartFile;

import com.google.common.util.concurrent.ThreadFactoryBuilder;

import okhttp3.ConnectionPool;
import okhttp3.Dispatcher;
import okhttp3.FormBody;
import okhttp3.MediaType;
import okhttp3.MultipartBody;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.RequestBody;
import okhttp3.Response;

/**
 * HTTP interface calling utility implemented with OKHttp
 */
public class OKHttpManager implements HttpManager {

    private final OkHttpClient okHttpClient;

    // Connection pool configuration
    private static final int MAX_IDLE_CONNECTIONS = 100;  // Maximum number of idle connections; excess idle connections will be cleared
    private static final long KEEP_ALIVE_DURATION = 5;    // Duration (in minutes) to keep idle connections alive; excess will be cleared

    // Thread pool configuration
    private static final int CORE_POOL_SIZE = 10;         // Core thread count; the number of threads that remain even when idle
    private static final int MAX_POOL_SIZE = 50;          // Maximum thread count; the maximum number of threads that can be created during peak times
    private static final int KEEP_ALIVE_TIME = 60;        // Time (in seconds) for threads exceeding the core count to remain alive when idle

    public OKHttpManager() {
        this(Duration.ofSeconds(30));
    }

    public OKHttpManager(Duration timeout) {
        // Create a custom thread pool for handling HTTP requests
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                CORE_POOL_SIZE,
                MAX_POOL_SIZE,
                KEEP_ALIVE_TIME, TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(),
                new ThreadFactoryBuilder().setNameFormat("OkHttp-Pool-%d").build()
        );

        // Create a custom Dispatcher to control request dispatching and concurrency
        Dispatcher dispatcher = new Dispatcher(threadPoolExecutor);

        this.okHttpClient = new OkHttpClient.Builder()
                .connectionPool(new ConnectionPool(MAX_IDLE_CONNECTIONS, KEEP_ALIVE_DURATION, TimeUnit.MINUTES))
                .dispatcher(dispatcher)
                .connectTimeout(timeout.toMillis(), TimeUnit.MILLISECONDS)  // Connection timeout
                .readTimeout(timeout.toMillis(), TimeUnit.MILLISECONDS)     // Read timeout
                .writeTimeout(timeout.toMillis(), TimeUnit.MILLISECONDS)    // Write timeout
                .build();
    }

    /**
     * Close OkHttpClient resources
     * Call this when the application shuts down to ensure resources are released correctly
     */
    public void shutdown() {
        if (okHttpClient != null) {
            okHttpClient.dispatcher().executorService().shutdown();  // Shut down the thread pool
            okHttpClient.connectionPool().evictAll();                // Clear all connections
        }
    }

    @Override
    public <T> T get(String url, Object queryParams, Class<T> responseType) {
        return get(url, queryParams, responseType, false);
    }

    @Override
    public <T> T get(String url, Object queryParams, Class<T> responseType, boolean encodeParams) {
        return get(url, queryParams, responseType, encodeParams, new HashMap<>());
    }

    @Override
    public <T> T get(String url, Object queryParams, Class<T> responseType, boolean encodeParams, HttpHeaders headers) {
        Map<String, String> headerMap = new HashMap<>();
        headers.forEach((key, values) -> headerMap.put(key, String.join(",", values)));
        return get(url, queryParams, responseType, encodeParams, headerMap);
    }

    @Override
    public <T> T get(String url, Object queryParams, Class<T> responseType, boolean encodeParams, Map<String, String> headers) {
        return get(url, queryParams, responseType, encodeParams, false, headers);
    }

    @Override
    public <T> T getWithBase64Response(String url, Object queryParams, Class<T> responseType, boolean encodeParams) {
        return get(url, queryParams, responseType, encodeParams, true);
    }

    @Override
    public <T> T getWithBase64Response(String url, Object queryParams, Class<T> responseType, boolean encodeParams, HttpHeaders headers) {
        Map<String, String> headerMap = new HashMap<>();
        headers.forEach((key, values) -> headerMap.put(key, String.join(",", values)));
        return getWithBase64Response(url, queryParams, responseType, encodeParams, headerMap);
    }

    @Override
    public <T> T getWithBase64Response(String url, Object queryParams, Class<T> responseType, boolean encodeParams, Map<String, String> headers) {
        return get(url, queryParams, responseType, encodeParams, true, headers);
    }

    @Override
    public <T> T get(String url, Object queryParams, Class<T> responseType, boolean encodeParams, boolean decodeResponse) {
        return get(url, queryParams, responseType, encodeParams, decodeResponse, new HashMap<>());
    }

    @Override
    public <T> T get(String url, Object queryParams, Class<T> responseType, boolean encodeParams, boolean decodeResponse, HttpHeaders headers) {
        Map<String, String> headerMap = new HashMap<>();
        headers.forEach((key, values) -> headerMap.put(key, String.join(",", values)));
        return get(url, queryParams, responseType, encodeParams, decodeResponse, headerMap);
    }

    @Override
    public <T> T get(String url, Object queryParams, Class<T> responseType, boolean encodeParams, boolean decodeResponse, Map<String, String> headers) {
        try {
            String finalUrl = url;
            if (queryParams != null) {
                String queryString;
                if (encodeParams) {
                    queryString = encodeBase64(JsonUtil.toJson(queryParams));
                } else {
                    MultiValueMap<String, String> params = toMultiValueMap(queryParams);
                    StringBuilder sb = new StringBuilder();
                    params.forEach((key, values) -> values.forEach(value -> {
                        if (sb.length() > 0) {
                            sb.append("&");
                        }
                        sb.append(key).append("=").append(value);
                    }));
                    queryString = sb.toString();
                }
                finalUrl = finalUrl + (finalUrl.contains("?") ? "&" : "?") + queryString;
            }

            Request.Builder requestBuilder = new Request.Builder()
                    .url(finalUrl)
                    .get();

            headers.forEach(requestBuilder::addHeader);

            Response response = okHttpClient.newCall(requestBuilder.build()).execute();
            String responseBody = response.body().string();

            if (!response.isSuccessful()) {
                throw new RuntimeException("GET request failed with status: " + response.code() + ", body: " + responseBody);
            }

            if (decodeResponse) {
                responseBody = decodeBase64(responseBody);
            }

            return JsonUtil.fromJson(responseBody, responseType);
        } catch (IOException e) {
            throw new RuntimeException("Error executing GET request", e);
        }
    }

    @Override
    public <T> T getWithBase64Params(String url, Object queryParams, Class<T> responseType) {
        return get(url, queryParams, responseType);
    }

    @Override
    public <T> T getWithBase64ParamsAndResponse(String url, Object queryParams, Class<T> responseType) {
        return get(url, queryParams, responseType, true, true);
    }

    @Override
    public <T> T getWithBase64Params(String url, Object queryParams, Class<T> responseType, HttpHeaders headers) {
        Map<String, String> headerMap = new HashMap<>();
        headers.forEach((key, values) -> headerMap.put(key, String.join(",", values)));
        return getWithBase64Params(url, queryParams, responseType, headerMap);
    }

    @Override
    public <T> T getWithBase64Params(String url, Object queryParams, Class<T> responseType, Map<String, String> headers) {
        return get(url, queryParams, responseType, true, false, headers);
    }

    @Override
    public <T> T getWithBase64ParamsAndResponse(String url, Object queryParams, Class<T> responseType, HttpHeaders headers) {
        Map<String, String> headerMap = new HashMap<>();
        headers.forEach((key, values) -> headerMap.put(key, String.join(",", values)));
        return getWithBase64ParamsAndResponse(url, queryParams, responseType, headerMap);
    }

    @Override
    public <T> T getWithBase64ParamsAndResponse(String url, Object queryParams, Class<T> responseType, Map<String, String> headers) {
        return get(url, queryParams, responseType, true, true, headers);
    }

    @Override
    public <T> T post(String url, Object body, Class<T> responseType) {
        return post(url, body, responseType, new HashMap<>(), false, false);
    }

    @Override
    public <T> T post(String url, Object body, Class<T> responseType, HttpHeaders headers) {
        Map<String, String> headerMap = new HashMap<>();
        headers.forEach((key, values) -> headerMap.put(key, String.join(",", values)));
        return post(url, body, responseType, headerMap);
    }

    @Override
    public <T> T post(String url, Object body, Class<T> responseType, Map<String, String> headers) {
        return post(url, body, responseType, headers, false, false);
    }

    @Override
    public <T> T post(String url, Object body, Class<T> responseType, HttpHeaders headers, boolean encodeBody, boolean decodeResponse) {
        Map<String, String> headerMap = new HashMap<>();
        headers.forEach((key, values) -> headerMap.put(key, String.join(",", values)));
        return post(url, body, responseType, headerMap, encodeBody, decodeResponse);
    }

    @Override
    public <T> T post(String url, Object body, Class<T> responseType, Map<String, String> headers, boolean encodeBody, boolean decodeResponse) {
        try {
            String bodyString = (body != null) ? JsonUtil.toJson(body) : "";

            if (encodeBody) {
                bodyString = encodeBase64(bodyString);
            }

            MediaType mediaType = MediaType.parse("application/json; charset=utf-8");
            RequestBody requestBody = RequestBody.create(bodyString, mediaType);

            Request.Builder requestBuilder = new Request.Builder()
                    .url(url)
                    .post(requestBody);

            headers.forEach(requestBuilder::addHeader);

            Response response = okHttpClient.newCall(requestBuilder.build()).execute();
            String responseBody = response.body().string();

            if (!response.isSuccessful()) {
                throw new RuntimeException("POST request failed with status: " + response.code() + ", body: " + responseBody);
            }

            if (decodeResponse) {
                responseBody = decodeBase64(responseBody);
            }

            return JsonUtil.fromJson(responseBody, responseType);
        } catch (IOException e) {
            throw new RuntimeException("Error executing POST request", e);
        }
    }

    @Override
    public <T> T postWithBase64Body(String url, Object body, Class<T> responseType) {
        return post(url, body, responseType, new HashMap<>(), true, false);
    }

    @Override
    public <T> T postWithBase64BodyAndResponse(String url, Object body, Class<T> responseType) {
        return post(url, body, responseType, new HashMap<>(), true, true);
    }

    @Override
    public <T> T postWithBase64Body(String url, Object body, Class<T> responseType, HttpHeaders headers) {
        Map<String, String> headerMap = new HashMap<>();
        headers.forEach((key, values) -> headerMap.put(key, String.join(",", values)));
        return postWithBase64Body(url, body, responseType, headerMap);
    }

    @Override
    public <T> T postWithBase64Body(String url, Object body, Class<T> responseType, Map<String, String> headers) {
        return post(url, body, responseType, headers, true, false);
    }

    @Override
    public <T> T postWithBase64BodyAndResponse(String url, Object body, Class<T> responseType, Map<String, String> headers) {
        return post(url, body, responseType, headers, true, true);
    }

    /**
     * Send a POST request in form format
     * Content-Type is "application/x-www-form-urlencoded"
     *
     * @param url Request URL
     * @param formData Form data
     * @param responseType Response type
     * @param <T> Response type generic
     * @return Response object
     */
    @Override
    public <T> T postForm(String url, Map<String, String> formData, Class<T> responseType) {
        return postForm(url, formData, responseType, new HashMap<>(), false);
    }

    /**
     * Send a form format POST request with custom headers
     *
     * @param url Request URL
     * @param formData Form data
     * @param responseType Response type
     * @param headers Request headers
     * @param <T> Response type generic
     * @return Response object
     */
    @Override
    public <T> T postForm(String url, Map<String, String> formData, Class<T> responseType, HttpHeaders headers) {
        Map<String, String> headerMap = new HashMap<>();
        headers.forEach((key, values) -> headerMap.put(key, String.join(",", values)));
        return postForm(url, formData, responseType, headerMap, false);
    }

    /**
     * Send a form format POST request with custom headers and an option to decode Base64 response
     *
     * @param url Request URL
     * @param formData Form data
     * @param responseType Response type
     * @param headers Request headers
     * @param decodeResponse Whether to decode Base64 response
     * @param <T> Response type generic
     * @return Response object
     */
    @Override
    public <T> T postForm(String url, Map<String, String> formData, Class<T> responseType, Map<String, String> headers, boolean decodeResponse) {
        try {
            FormBody.Builder formBodyBuilder = new FormBody.Builder();
            if (formData != null) {
                formData.forEach((key, value) -> {
                    if (value != null) {
                        formBodyBuilder.add(key, value);
                    }
                });
            }

            RequestBody requestBody = formBodyBuilder.build();

            Request.Builder requestBuilder = new Request.Builder()
                    .url(url)
                    .post(requestBody);

            // Add Content-Type header to ensure it's in form format
            requestBuilder.header("Content-Type", "application/x-www-form-urlencoded");

            // Add custom request headers
            headers.forEach(requestBuilder::addHeader);

            Response response = okHttpClient.newCall(requestBuilder.build()).execute();
            String responseBody = response.body().string();

            if (!response.isSuccessful()) {
                throw new RuntimeException("POST form request failed with status: " + response.code() + ", body: " + responseBody);
            }

            if (decodeResponse) {
                responseBody = decodeBase64(responseBody);
            }

            return JsonUtil.fromJson(responseBody, responseType);
        } catch (IOException e) {
            throw new RuntimeException("Error executing POST form request", e);
        }
    }

    /**
     * Send a form format POST request and decode Base64 response
     *
     * @param url Request URL
     * @param formData Form data
     * @param responseType Response type
     * @param <T> Response type generic
     * @return Response object
     */
    @Override
    public <T> T postFormWithBase64Response(String url, Map<String, String> formData, Class<T> responseType) {
        return postForm(url, formData, responseType, new HashMap<>(), true);
    }

    /**
     * Send a form format POST request with custom headers and decode Base64 response
     *
     * @param url Request URL
     * @param formData Form data
     * @param responseType Response type
     * @param headers Request headers
     * @param <T> Response type generic
     * @return Response object
     */
    @Override
    public <T> T postFormWithBase64Response(String url, Map<String, String> formData, Class<T> responseType, HttpHeaders headers) {
        Map<String, String> headerMap = new HashMap<>();
        headers.forEach((key, values) -> headerMap.put(key, String.join(",", values)));
        return postForm(url, formData, responseType, headerMap, true);
    }

    /**
     * Send a form format POST request using an object as form data
     * The object will be converted to Map<String, String> format
     *
     * @param url Request URL
     * @param formObject Form data object
     * @param responseType Response type
     * @param <T> Response type generic
     * @return Response object
     */
    @Override
    public <T> T postFormObject(String url, Object formObject, Class<T> responseType) {
        Map<String, String> formData = objectToFormMap(formObject);
        return postForm(url, formData, responseType);
    }

    /**
     * Send a form format POST request with custom headers using an object as form data
     *
     * @param url Request URL
     * @param formObject Form data object
     * @param responseType Response type
     * @param headers Request headers
     * @param <T> Response type generic
     * @return Response object
     */
    @Override
    public <T> T postFormObject(String url, Object formObject, Class<T> responseType, HttpHeaders headers) {
        Map<String, String> formData = objectToFormMap(formObject);
        return postForm(url, formData, responseType, headers);
    }

    /**
     * Convert an object to a form map
     */
    private Map<String, String> objectToFormMap(Object object) {
        Map<String, String> result = new HashMap<>();
        try {
            Map<String, Object> map = JsonUtil.fromJson(object, Map.class);
            map.forEach((key, value) -> {
                if (value != null) {
                    result.put(key, String.valueOf(value));
                }
            });
            return result;
        } catch (Exception e) {
            throw new RuntimeException("Failed to convert object to form map", e);
        }
    }

    private MultiValueMap<String, String> toMultiValueMap(Object object) {
        try {
            MultiValueMap<String, String> params = new LinkedMultiValueMap<>();
            Map<String, String> map = JsonUtil.toMap(JsonUtil.toJson(object), String.class);
            map.forEach((key, value) -> {
                if (value != null) {
                    params.add(key, value);
                }
            });
            return params;
        } catch (Exception e) {
            throw new RuntimeException("Failed to convert object to MultiValueMap", e);
        }
    }

    private String encodeBase64(String input) {
        return Base64.getEncoder().encodeToString(input.getBytes());
    }

    private String decodeBase64(String input) {
        return new String(Base64.getDecoder().decode(input));
    }

    /**
     * Send POST request with files
     *
     * @param <T>            Response type generic
     * @param url            Request URL
     * @param formData       Form data
     * @param file           File data, key is field name, value is MultipartFile object
     * @param responseType   Response type
     * @param headers        Request headers
     * @param decodeResponse Whether to decode Base64 response
     * @return Response object
     */
    public <T> T postFile(String url, Map<String, String> formData, MultipartFile file, Class<T> responseType, Map<String, String> headers, boolean decodeResponse) {
        try {
            MultipartBody.Builder multipartBuilder = new MultipartBody.Builder()
                    .setType(MultipartBody.FORM);

            // Add form data
            if (formData != null) {
                formData.forEach((key, value) -> {
                    if (value != null) {
                        multipartBuilder.addFormDataPart(key, value);
                    }
                });
            }

            // Add file
            if (file != null && !file.isEmpty()) {
                try {
                    multipartBuilder.addFormDataPart(
                            "file",
                            file.getOriginalFilename(),
                            RequestBody.create(
                                    MediaType.parse(file.getContentType()),
                                    file.getBytes()
                            )
                    );
                } catch (IOException e) {
                    throw new RuntimeException("Error reading file: " + file.getOriginalFilename(), e);
                }
            }

            RequestBody requestBody = multipartBuilder.build();

            Request.Builder requestBuilder = new Request.Builder()
                    .url(url)
                    .post(requestBody);

            // Add custom headers
            headers.forEach(requestBuilder::addHeader);

            Response response = okHttpClient.newCall(requestBuilder.build()).execute();
            String responseBody = response.body().string();

            if (!response.isSuccessful()) {
                throw new RuntimeException("POST file request failed with status: " + response.code() + ", body: " + responseBody);
            }

            if (decodeResponse) {
                responseBody = decodeBase64(responseBody);
            }

            return JsonUtil.fromJson(responseBody, responseType);
        } catch (IOException e) {
            throw new RuntimeException("Error executing POST file request", e);
        }
    }

    @Override
    public <T> T postFileObject(String url, Object formObject, MultipartFile file, Class<T> responseType, Map<String, String> headers, boolean decodeResponse) {
        Map<String, String> formData = objectToFormMap(formObject);
        return postFile(url, formData, file, responseType, headers, decodeResponse);
    }
}